//==================================================================
/// image.cpp
///
/// Created by Davide Pasca - 2009/10/5
/// See the file "license.txt" that comes with this project for
/// copyright info. 
//==================================================================

#include "stdafx.h"
#include <memory>
#include "image.h"
#include "png.h"

//==================================================================
static image *PngLoadImage(
			const char *pstrFileName,
			png_color *pBkgColor );

//==================================================================
image::image()
{
	mW				= 0;
	mH				= 0;
	mDepth			= 0;
	mBytesPerPixel	= 0;
	mBytesPerRow	= 0;
}

//==================================================================
image::image( const image &from )
{
	mW				= from.mW;
	mH				= from.mH;
	mDepth			= from.mDepth;
	mBytesPerPixel	= from.mBytesPerPixel;
	mBytesPerRow	= from.mBytesPerRow;
	mPixels		= from.mPixels;
}

//==================================================================
image::image( U32 w, U32 h, U32 depth, U32 rowPitch )
{
	DASSERT( (depth & 7) == 0 );

	mW				= w;
	mH				= h;
	mDepth			= depth;
	mBytesPerPixel	= mDepth / 8;

	// force to 4 bytes per pixel for now..
	if ( mBytesPerPixel == 3 )
	{
		mBytesPerPixel = 4;
		mBytesPerRow = w * mBytesPerPixel;
	}
	else
	{
		if ( rowPitch )
		{
			DASSERT( rowPitch <= w * mBytesPerPixel );
			mBytesPerRow = rowPitch;
		}
		else
			mBytesPerRow = w * mBytesPerPixel;
	}

	mPixels.resize( mBytesPerRow * mH );

	for (size_t i=0; i < mPixels.size(); ++i)
		mPixels[i] = 0;
}

//==================================================================
image::image( const DVec<U8> &pixels, U32 w, U32 h )
{
	mW				= w;
	mH				= h;
	mDepth			= 8;
	mBytesPerPixel	= mDepth / 8;
	mBytesPerRow	= w * mBytesPerPixel;
	mPixels			= pixels;
}

//==================================================================
image::image( const DStr &fname, U32 forcedDepth )
{
	mW				= 0;
	mH				= 0;
	mDepth			= 0;
	mBytesPerPixel	= 0;
	mBytesPerRow	= 0;

	image *pImg = PngLoadImage(
						fname.c_str(),
						NULL );

	if ( forcedDepth )
	{
		pImg->ConvertToDepth( forcedDepth );
	}

	*this = *pImg;

	DSAFE_DELETE( pImg );
}

//==================================================================
void image::ConvertToDepth( U32 forcedDepth )
{
	if ( mDepth == forcedDepth )
		return;

	U32 newDepth = forcedDepth;
	U32 newBBP	 = newDepth / 8;
	U32 newBPR	 = mW * newBBP;

	DVec<U8>	tmp;

	tmp.resize( newBPR * mH );

	if ( newDepth == 8 )
	{
		for (U32 y=0; y < mH; ++y)
		{
			for (U32 x=0; x < mW; ++x)
			{
				U8	col = *GetPixelPtr( x, y );
				tmp[ x * newBBP + y * newBPR ] = col;
			}
		}
	}
	else
	{
		DASSERT( 0 );
	}

	mDepth			= newDepth	;
	mBytesPerPixel	= newBBP	;
	mBytesPerRow	= newBPR	;

	mPixels		= tmp;
}

//==================================================================
static void png_cexcept_error(png_structp png_ptr, png_const_charp msg)
{
	DASSTHROW( 0, ("PNG Error. %s", msg) );
}

//==================================================================
class FileWrapper
{
public:
	FILE	*mpFile;

	FileWrapper( const char *pFileName, const char *pMode ) :
		mpFile(NULL)
	{
		if ( fopen_s( &mpFile, pFileName, pMode ) )
			DASSTHROW( 0, ("Failed to open %s", pFileName) );
	}

	~FileWrapper()
	{
		if ( mpFile )
			fclose( mpFile );
	}

	operator FILE*()
	{
		return mpFile;
	}
};

//==================================================================
static image *PngLoadImage(
			const char *pstrFileName,
			png_color *pBkgColor )
{
	adel<image>				pImage;

	png_byte				pbSig[8];
	int						iBitDepth;
	int						iColorType;
	double					dGamma;
	png_uint_32				ulChannels;
	png_uint_32				ulRowBytes;

	DVec<U8*>				pRowPointers;

    // open the PNG input file

	FileWrapper		file( pstrFileName, "rb" );

	// first check the eight byte PNG signature

	if ( 8 != fread(pbSig, 1, 8, file) )
	{
		DASSTHROW( 0, ("File %s is not a valid PNG file", pstrFileName) );
	}

	if NOT( png_check_sig(pbSig, 8) )
	{
		DASSTHROW( 0, ("File %s is not a valid PNG file", pstrFileName) );
	}

	// create the two png(-info) structures

	png_structp png_ptr =
		png_create_read_struct(
					PNG_LIBPNG_VER_STRING,
					NULL,
					(png_error_ptr)png_cexcept_error,
					(png_error_ptr)NULL );

	if NOT( png_ptr )
	{
		DASSTHROW( 0, ("Failed to create the structure") );
	}

	png_infop info_ptr = png_create_info_struct(png_ptr);

	if NOT( info_ptr )
	{
		png_destroy_read_struct(&png_ptr, NULL, NULL);
		DASSTHROW( 0, ("Failed to the info structure") );
	}

    try
    {
        
        // initialize the png structure
        
#if !defined(PNG_NO_STDIO)
        png_init_io(png_ptr, file);
#else
        png_set_read_fn(png_ptr, (png_voidp)file(), png_read_data);
#endif
        
        png_set_sig_bytes(png_ptr, 8);
        
        // read all PNG info up to image data
        
        png_read_info(png_ptr, info_ptr);
        
        // get width, height, bit-depth and color-type
        
		png_uint_32	wd = 0;
		png_uint_32	he = 0;

        png_get_IHDR(
				png_ptr,
				info_ptr,
				&wd,
				&he,
				&iBitDepth,
				&iColorType,
				NULL,
				NULL,
				NULL );

        // expand images of all color-type and bit-depth to 3x8 bit RGB images
        // let the library process things like alpha, transparency, background
        
        if (iBitDepth == 16)
            png_set_strip_16(png_ptr);

        if (iColorType == PNG_COLOR_TYPE_PALETTE)
            png_set_expand(png_ptr);

		if (iBitDepth < 8)
            png_set_expand(png_ptr);

		if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
            png_set_expand(png_ptr);

		if (iColorType == PNG_COLOR_TYPE_GRAY ||
            iColorType == PNG_COLOR_TYPE_GRAY_ALPHA)
            png_set_gray_to_rgb(png_ptr);
        
        // set the background color to draw transparent and alpha images over.
/*
		png_color_16			*pBackground;
    
		if (png_get_bKGD(png_ptr, info_ptr, &pBackground))
        {
            png_set_background(png_ptr, pBackground, PNG_BACKGROUND_GAMMA_FILE, 1, 1.0);
			if ( pBkgColor )
			{
				pBkgColor->red   = (U8) pBackground->red;
				pBkgColor->green = (U8) pBackground->green;
				pBkgColor->blue  = (U8) pBackground->blue;
			}
        }
        else
        {
            pBkgColor = NULL;
        }
*/
        
        // if required set gamma conversion
        if (png_get_gAMA(png_ptr, info_ptr, &dGamma))
            png_set_gamma(png_ptr, (double) 2.2, dGamma);
        
        // after the transformations have been registered update info_ptr data
        
        png_read_update_info(png_ptr, info_ptr);
        
        // get again width, height and the new bit-depth and color-type
        
        png_get_IHDR(
				png_ptr,
				info_ptr,
				&wd,
				&he,
				&iBitDepth,
				&iColorType,
				NULL,
				NULL,
				NULL);
        
        // row_bytes is the width x number of channels
        
        ulRowBytes = png_get_rowbytes(png_ptr, info_ptr);
        ulChannels = png_get_channels(png_ptr, info_ptr);
        
        // now we can allocate memory to store the image

		pImage = new image( wd, he, ulChannels * iBitDepth, ulRowBytes );

        // and allocate memory for an array of row-pointers
        
		pRowPointers.resize( he );
        
        // set the individual row-pointers to point at the correct offsets
        for (U32 i = 0; i < he; i++)
            pRowPointers[i] = pImage->GetPixelPtr( 0, i );
        
        // now we can go ahead and just read the whole image
        
        png_read_image( png_ptr, &pRowPointers[0] );
        
        // read the additional chunks in the PNG file (not really needed)
        
        png_read_end( png_ptr, NULL );

		if ( ulChannels == 3 && iBitDepth == 8 && pImage->mBytesPerPixel == 4 )
		{
			for (U32 y=0; y < he; ++y)
			{
				U8	*pRow = pImage->GetPixelPtr( 0, y );

				for (U32 i=wd; i > 0; --i)
				{
					U32 x = i - 1;

					U8	r = pRow[ x * 3 + 0 ];
					U8	g = pRow[ x * 3 + 1 ];
					U8	b = pRow[ x * 3 + 2 ];

					pRow[ x * 4 + 0 ] = r;
					pRow[ x * 4 + 1 ] = g;
					pRow[ x * 4 + 2 ] = b;
					pRow[ x * 4 + 3 ] = 0;
				}
			}
			
		}
    }
    catch ( ... )
    {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);

        throw;
    }

    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);

	return pImage.release();
}
